//################################################################################
// MIT License
// Copyright (c) 2024 ZhangYihua
//
// Change Logs:
// Date           Author       Notes
// 2020-06-06     ZhangYihua   first version
//
// Description  : Async FIFO controller
//################################################################################

module afifo_ctrl #(
parameter           DEPTH                   = 32,       // must DEPTH=2*n (n>=1)
parameter           SYNC_NUM_W2R            = 3,        // normally DEPTH>=(SYNC_NUM*2+4) is required for full speed scene
parameter           SYNC_NUM_R2W            = 3,
parameter           GRAY_ADDR_LPL           = 0,        // if RAPL=WAPL=0, let GRAY_ADDR_LPL=0, else 1

// the following parameters are calculated automatically
parameter           ADDR_BW                 = $clog2(DEPTH),    // address range [0:DEPTH-1]
parameter           CNT_BW                  = $clog2(DEPTH + 1) // counter range [0:DEPTH]
) ( 
input                                       rst_wr_n,
input                                       clk_wr,

input                                       fifo_wen,
output                                      fifo_full,
output                                      fifo_afull,         // afull==0 means free entry in FIFO is at least 'DEPTH-afull_th'
output              [CNT_BW-1:0]            fifo_wcnt,          // wcnt is not include synchronization pipeline number

// RAPL:    read address pipeline means pipeline steps from mem_raddr to SRAM, 0 means no DFF between them
// WAPL:    write address pipeline means pipeline steps from mem_waddr to SRAM, 0 means no DFF between them
// it's required that RAPL must equals to WAPL at any case.
// case 1: RAPL=WAPL=0, normal operation is ok;
// case 2: RAPL=WAPL=1 without prefetch or prefetch mode is 'RDPL_CACHE', let GRAY_ADDR_LPL = 1;
// case 3: RAPL=WAPL=1 with prefetch mode 'RDPL_SHIFT' or 'RDPL_STUFF', let GRAY_ADDR_LPL = 1, let DEPTH = theory_estimation + 1, and
//                     take afull as back pressure to avoid SRAM to overflow;
// case others: not recommended;
output                                      mem_wen,
output              [ADDR_BW-1:0]           mem_waddr,

input               [ADDR_BW-1:0]           cfg_fifo_afull_th,  // afull_th<fifo_wcnt<=DEPTH is afull, range is [0:DEPTH-1]
output                                      err_fifo_wilf,      // report internal logic fault, and fifo can resotre automatically
output                                      err_fifo_wof,       // fifo write overflow

input                                       rst_rd_n,
input                                       clk_rd,

input                                       fifo_ren,
output                                      fifo_empty,
output                                      fifo_aempty,        // aempty==0 means data number in FIFO is at least 'aempty_th' 
output              [CNT_BW-1:0]            fifo_rcnt,          // rcnt is not include synchronization pipeline number

output                                      mem_ren,
output              [ADDR_BW-1:0]           mem_raddr,

input               [ADDR_BW-1:0]           cfg_fifo_aempty_th, // 0<=fifo_cnt<aempty_th is aempty, range is [1:min(DEPTH,2^ADDR_BW-1)]
output                                      err_fifo_rilf,      // report internal logic fault, and fifo can resotre automatically
output                                      err_fifo_ruf        // fifo read underflow
);

//################################################################################
// define local varialbe and localparam
//################################################################################
localparam          DEPTH_E                 = 1<<ADDR_BW;
localparam          ADDR_OFST               = (DEPTH_E-DEPTH)/2;
localparam  [ADDR_BW+1-1:0] ADDR_BIN_INI    = ADDR_OFST;
localparam  [ADDR_BW+1-1:0] ADDR_GRY_INI    = ADDR_BIN_INI^(ADDR_BIN_INI>>1);
localparam  [ADDR_BW+1-1:0] DEPTH_V         = DEPTH;

wire                [ADDR_BW+1-1:0]         raddr_g_r2w;
wire                [ADDR_BW+1-1:0]         raddr_r2w;
wire                                        wen;
wire                                        waddr_ce;
wire                [ADDR_BW+1-1:0]         waddr_c;
wire                [ADDR_BW+1-1:0]         wdiff;
reg                 [ADDR_BW+1-1:0]         waddr;
wire                [ADDR_BW+1-1:0]         waddr_g;
reg                 [ADDR_BW+1-1:0]         wcnt;
wire                [ADDR_BW+1-1:0]         afull_th;
wire                                        w_big_r;
wire                                        addr_full;
reg                                         full;
reg                                         afull;

wire                [ADDR_BW+1-1:0]         waddr_g_w2r;
wire                [ADDR_BW+1-1:0]         waddr_w2r;
wire                                        ren;
wire                                        raddr_ce;
wire                [ADDR_BW+1-1:0]         raddr_c;
wire                [ADDR_BW+1-1:0]         rdiff;
reg                 [ADDR_BW+1-1:0]         raddr;
wire                [ADDR_BW+1-1:0]         raddr_g;
reg                 [ADDR_BW+1-1:0]         rcnt;
reg                                         empty;
wire                [ADDR_BW+1-1:0]         aempty_th;
reg                                         aempty;
reg                                         frc_empty;

//################################################################################
// main
//################################################################################

generate if (GRAY_ADDR_LPL==0) begin:G_LPL0
    wire                [ADDR_BW+1-1:0]         raddr_nxt;
    wire                [ADDR_BW+1-1:0]         waddr_nxt;

    assign raddr_nxt = (raddr_ce==1'b1) ? raddr_c : raddr;
    assign waddr_nxt = (waddr_ce==1'b1) ? waddr_c : waddr;

    bin2gray #(.BW(ADDR_BW+1), .REG_EN(1'b1), .GRY_INI(ADDR_GRY_INI)) u_r2w_gray (.rst_n(rst_rd_n), .clk(clk_rd), .i_bin(raddr_nxt), .o_gray(raddr_g));
    bin2gray #(.BW(ADDR_BW+1), .REG_EN(1'b1), .GRY_INI(ADDR_GRY_INI)) u_w2r_gray (.rst_n(rst_wr_n), .clk(clk_wr), .i_bin(waddr_nxt), .o_gray(waddr_g));

end else begin:G_LPL1
    
    bin2gray #(.BW(ADDR_BW+1), .REG_EN(1'b1), .GRY_INI(ADDR_GRY_INI)) u_r2w_gray (.rst_n(rst_rd_n), .clk(clk_rd), .i_bin(raddr), .o_gray(raddr_g));
    bin2gray #(.BW(ADDR_BW+1), .REG_EN(1'b1), .GRY_INI(ADDR_GRY_INI)) u_w2r_gray (.rst_n(rst_wr_n), .clk(clk_wr), .i_bin(waddr), .o_gray(waddr_g));

end endgenerate

sync_dff #(.SYNC_NUM(SYNC_NUM_R2W), .BW(ADDR_BW+1), .INI(ADDR_GRY_INI)) u_r2w_dff (.rst_n(rst_wr_n), .clk(clk_wr), .d(raddr_g), .q(raddr_g_r2w));
sync_dff #(.SYNC_NUM(SYNC_NUM_W2R), .BW(ADDR_BW+1), .INI(ADDR_GRY_INI)) u_w2r_dff (.rst_n(rst_rd_n), .clk(clk_rd), .d(waddr_g), .q(waddr_g_w2r));

gray2bin #(.BW(ADDR_BW+1)) u_r2w_bin (.i_gray(raddr_g_r2w), .o_bin(raddr_r2w));
gray2bin #(.BW(ADDR_BW+1)) u_w2r_bin (.i_gray(waddr_g_w2r), .o_bin(waddr_w2r));

assign wen = fifo_wen & (~full);
assign ren = fifo_ren & (~empty);

generate if (DEPTH==DEPTH_E) begin:G_PWR2   // DEPTH == 2^ADDR_BW

    assign waddr_ce = wen;
    assign raddr_ce = ren;

    assign waddr_c = waddr + 1'd1;
    assign raddr_c = raddr + 1'd1;

    assign wdiff = waddr     - raddr_r2w;
    assign rdiff = waddr_w2r - raddr;

    assign mem_waddr = waddr[0+:ADDR_BW];
    assign mem_raddr = raddr[0+:ADDR_BW];

end else begin:G_NPWR2   // DEPTH < 2^ADDR_BW

    localparam  [ADDR_BW-1:0]   ADDR_MAX        = ADDR_OFST+DEPTH-1;
    localparam  [ADDR_BW-1:0]   ADDR_MIN        = ADDR_OFST;
    localparam  [ADDR_BW+1-1:0] ADDR_OFSTX2     = ADDR_OFST*2;

    wire                [ADDR_BW+1-1:0]         waddr_sub;
    wire                                        wcross;
    wire                [ADDR_BW+1-1:0]         raddr_add;
    wire                                        rcross;

    // recover from illegal address
    assign waddr_ce = ((waddr[0+:ADDR_BW]>ADDR_MAX) || (waddr[0+:ADDR_BW]<ADDR_MIN)) ? 1'b1 : wen;
    assign raddr_ce = ((raddr[0+:ADDR_BW]>ADDR_MAX) || (raddr[0+:ADDR_BW]<ADDR_MIN)) ? 1'b1 : ren;

    assign waddr_c[0+:ADDR_BW] = (waddr[0+:ADDR_BW]>=ADDR_MAX) ? ADDR_MIN : (waddr[0+:ADDR_BW] + 1'd1);
    assign raddr_c[0+:ADDR_BW] = (raddr[0+:ADDR_BW]>=ADDR_MAX) ? ADDR_MIN : (raddr[0+:ADDR_BW] + 1'd1);

    assign waddr_c[ADDR_BW]    = (waddr[0+:ADDR_BW]>=ADDR_MAX) ? (~waddr[ADDR_BW]) : waddr[ADDR_BW];
    assign raddr_c[ADDR_BW]    = (raddr[0+:ADDR_BW]>=ADDR_MAX) ? (~raddr[ADDR_BW]) : raddr[ADDR_BW];
    
    assign waddr_sub = waddr - ADDR_OFSTX2;
    assign wcross    = (waddr[ADDR_BW]!=raddr_r2w[ADDR_BW]) ? 1'b1 : 1'b0;
    assign wdiff     = (wcross==1'b1) ? (waddr_sub - raddr_r2w) :
                                        (waddr     - raddr_r2w) ;   // waddr<raddr_r2w is illegal because (waddr-raddr_r2w)>DEPTH_E>FULL
                                                                    // it's better to ignore this condition branch for optimizing timing
    assign raddr_add = raddr + ADDR_OFSTX2;
    assign rcross    = (waddr_w2r[ADDR_BW]!=raddr[ADDR_BW]) ? 1'b1 : 1'b0;
    assign rdiff     = (rcross==1'b1) ? (waddr_w2r - raddr_add) :
                                        (waddr_w2r - raddr    ) ;   // same to (waddr - raddr_r2w)

    assign mem_waddr = waddr[0+:ADDR_BW] - ADDR_MIN;    // range is [0:DEPTH-1] after removing address offset
    assign mem_raddr = raddr[0+:ADDR_BW] - ADDR_MIN;    // range is [0:DEPTH-1] after removing address offset

end endgenerate

always@(posedge clk_wr or negedge rst_wr_n) begin
    if (rst_wr_n==1'b0) begin
        waddr <=`U_DLY ADDR_BIN_INI;
    end else begin
        if (waddr_ce==1'b1)
            waddr <=`U_DLY waddr_c;
        else
            ;
    end
end

always@(posedge clk_wr or negedge rst_wr_n) begin
    if (rst_wr_n==1'b0) begin
        wcnt <=`U_DLY {ADDR_BW+1{1'b0}};
    end else begin
        wcnt <=`U_DLY wdiff + {{ADDR_BW{1'b0}}, wen};
    end
end

assign w_big_r   = (waddr[0+:ADDR_BW]>=raddr_r2w[0+:ADDR_BW]) ? 1'b1 : 1'b0;
assign addr_full = (waddr[ADDR_BW]!=raddr_r2w[ADDR_BW]) ? w_big_r : (~w_big_r);
always@(posedge clk_wr or negedge rst_wr_n) begin
    if (rst_wr_n==1'b0) begin
        full <=`U_DLY 1'b0;
    end else begin
        if ((wen==1'b1) && (waddr_c=={(~raddr_r2w[ADDR_BW]), raddr_r2w[0+:ADDR_BW]}))
            full <=`U_DLY 1'b1;
        else
            full <=`U_DLY addr_full;
    end
end

assign afull_th = {1'b0, cfg_fifo_afull_th};
always@(posedge clk_wr or negedge rst_wr_n) begin
    if (rst_wr_n==1'b0) begin
        afull <=`U_DLY 1'b0;
    end else begin
        if ((wen==1'b1) && (wdiff==afull_th))
            afull <=`U_DLY 1'b1;
        else if (wdiff>afull_th)
            afull <=`U_DLY 1'b1;
        else
            afull <=`U_DLY 1'b0;
    end
end

// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------

always@(posedge clk_rd or negedge rst_rd_n) begin
    if (rst_rd_n==1'b0) begin
        raddr <=`U_DLY ADDR_BIN_INI;
    end else begin
        if (frc_empty==1'b1)            // recover from abnormal scene, like the following, 
            raddr <=`U_DLY waddr_w2r;   // wr and rd at same speed forever, wr side not aempty but rd side exceed full
        else if (raddr_ce==1'b1)
            raddr <=`U_DLY raddr_c;
        else
            ;
    end
end

always@(posedge clk_rd or negedge rst_rd_n) begin
    if (rst_rd_n==1'b0) begin
        rcnt <=`U_DLY {ADDR_BW+1{1'b0}};
    end else begin
        rcnt <=`U_DLY rdiff - {{ADDR_BW{1'b0}}, ren};
    end
end

always@(posedge clk_rd or negedge rst_rd_n) begin
    if (rst_rd_n==1'b0) begin
        empty <=`U_DLY 1'b1;
    end else begin
        if (frc_empty==1'b1)
            empty <=`U_DLY 1'b1;
        else if ((ren==1'b1) && (raddr_c==waddr_w2r))
            empty <=`U_DLY 1'b1;
        else if (raddr==waddr_w2r)
            empty <=`U_DLY 1'b1;
        else
            empty <=`U_DLY 1'b0;
    end
end

assign aempty_th     = {1'b0, cfg_fifo_aempty_th};
always@(posedge clk_rd or negedge rst_rd_n) begin
    if (rst_rd_n==1'b0) begin
        aempty <=`U_DLY 1'b1;
    end else begin
        if ((ren==1'b1) && (rdiff==aempty_th))
            aempty <=`U_DLY 1'b1;
        else if (rdiff<aempty_th)
            aempty <=`U_DLY 1'b1;
        else
            aempty <=`U_DLY 1'b0;
    end
end

always@(posedge clk_rd or negedge rst_rd_n) begin
    if (rst_rd_n==1'b0) begin
        frc_empty <=`U_DLY 1'b0;
    end else begin
        if (rcnt>DEPTH_V)
            frc_empty <=`U_DLY 1'b1;
        else
            frc_empty <=`U_DLY 1'b0;
    end
end
//assign frc_empty    = (rcnt>DEPTH_V) ? 1'b1 : 1'b0;

assign fifo_full    = full;
assign fifo_empty   = empty;
assign fifo_afull   = afull;
assign fifo_aempty  = aempty;

assign fifo_wcnt    = wcnt[0+:CNT_BW];
assign fifo_rcnt    = rcnt[0+:CNT_BW];

assign err_fifo_wof = fifo_wen & full;
assign err_fifo_ruf = fifo_ren & empty;

assign mem_wen      = wen;
assign mem_ren      = ren;

assign err_fifo_wilf = (wcnt>DEPTH_V) ? 1'b1 : 1'b0;
assign err_fifo_rilf = (rcnt>DEPTH_V) ? 1'b1 : 1'b0;

//################################################################################
// ASSERTION
//################################################################################

`ifdef CBB_ASSERT_ON
// synopsys translate_off

reg     OVERFLOW_CHKEN;
reg     UNDERFLOW_CHKEN;

a_depth_even: assert property (@(posedge clk_wr) disable iff (!rst_wr_n)
    (DEPTH%2==0)
) else begin
    $error("DEPTH is not even.");
    $stop;
end

a_overflow: assert property (@(posedge clk_wr) disable iff (!rst_wr_n)
    ((OVERFLOW_CHKEN!==1'b0)&fifo_wen |-> (~full))
) else begin
    $error("fifo is overflow.");
end

a_underflow: assert property (@(posedge clk_rd) disable iff (!rst_rd_n)
    ((UNDERFLOW_CHKEN!==1'b0)&fifo_ren |-> (~empty))
) else begin
    $error("fifo is underflow.");
end

// generally define CBB_DEBUG_ON only when debugging CBB
`ifdef CBB_DEBUG_ON

a_rcnt_rdiff_cmp: assert property (@(posedge clk_rd) disable iff (!rst_rd_n)
    ((rcnt + sva_cnt_f(waddr_w2r, $past(waddr_w2r))) == rdiff)
) else begin
    $error("fifo_rcnt is wrong.");
    $stop;
end

a_wcnt_wdiff_cmp: assert property (@(posedge clk_wr) disable iff (!rst_wr_n)
    ((wcnt - sva_cnt_f(raddr_r2w, $past(raddr_r2w))) == wdiff)
) else begin
    $error("fifo_wcnt is wrong.");
    $stop;
end

a_rcnt_less_sva: assert property (@(posedge clk_rd) disable iff (!rst_rd_n)
    (rcnt <= sva_cnt_f(waddr, raddr))
) else begin
    $error("fifo_rcnt is wrong.");
    $stop;
end

a_wcnt_big_sva: assert property (@(posedge clk_wr) disable iff (!rst_wr_n)
    (wcnt >= sva_cnt_f(waddr, raddr))
) else begin
    $error("fifo_wcnt is wrong.");
    $stop;
end

function integer sva_cnt_f;
    input   integer     waddr;
    input   integer     raddr;

    reg                 wsect;
    reg                 rsect;
    reg                 sect_same;
    reg                 w_big_r;
    begin
        wsect = (waddr>=DEPTH_E) ? 1'b1 : 1'b0;
        rsect = (raddr>=DEPTH_E) ? 1'b1 : 1'b0;

        sect_same = (wsect==rsect) ? 1'b1 : 1'b0;
        w_big_r   = (waddr>=raddr) ? 1'b1 : 1'b0;

        case({sect_same, w_big_r})
            2'b00 : sva_cnt_f = waddr - raddr + (DEPTH_E+DEPTH);
            2'b01 : sva_cnt_f = waddr - raddr - (DEPTH_E-DEPTH);
            2'b10 : sva_cnt_f = waddr - raddr + 2*DEPTH;
            2'b11 : sva_cnt_f = waddr - raddr;
        endcase
//        $display("%d, %d, %b, %b, %d", waddr, raddr, sect_same, w_big_r, sva_cnt_f);
    end
endfunction

`endif

// synopsys translate_on
`endif

endmodule

